/*
 * The MIT License
 *
 * Copyright (c) 2009 André D. Piske
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 *
 */
package org.gohtml.tree;

import java.util.LinkedList;
import java.util.List;
import java.util.regex.Pattern;
import org.gohtml.Attribute;

/**
 * Selector condition.
 * This will compare node properties and give a true/false result.
 *
 * @author André D. Piske
 * @version 0.2 alpha
 * @since 0.1 alpha
 */
class Condition {

    public static final int COND_ATTRIBUTE_EQUALS = 1;
    public static final int COND_NAME_EQUALS = 2;
    private static final int C_ATTR_EQUAL_SIMPLE = 1;
    private static final int C_ATTR_EQUAL_REGEX = 2;
    private static final int C_TAG_NAME_EQUALS_SIMPLE = 3;
    private static final int C_TAG_NAME_EQUALS_REGEX = 4;
    //
    private int type;
    private Object param;

    /**
     * @param type Must be COND_ATTRIBUTE_EQUALS or COND_NAME_EQUALS
     */
    public Condition(int type, Object param) {
        if (type == COND_ATTRIBUTE_EQUALS) {
            BlockElement e = (BlockElement) param;
            this.type = C_ATTR_EQUAL_SIMPLE;
            try {
                if (e.attribute.charAt(0) == '~') {
                    this.type = C_ATTR_EQUAL_REGEX;
                    e.attribute = e.attribute.substring(1);
                }
            } catch (Exception ex) {
            }
            this.param = e;
        } else {
            String name = (String) param;
            if (name.charAt(0) == '~') {
                this.param = name.substring(1);
                this.type = C_TAG_NAME_EQUALS_REGEX;
            } else {
                this.param = name;
                this.type = C_TAG_NAME_EQUALS_SIMPLE;
            }
        }
    }

    private boolean matchAttrEqualRegex(Node node) {
        BlockElement e = (BlockElement) this.param;
        Attribute attr = node.findAttribute(e.name);
        if (attr == null) {
            return false;
        }

        Pattern pat = Pattern.compile(e.attribute, Pattern.CASE_INSENSITIVE);
        return pat.matcher(attr.getValue()).matches();
    }

    private boolean matchAttrEqualSimple(Node node) {
        BlockElement e = (BlockElement) this.param;
        Attribute attr = node.findAttribute(e.name);
        return (attr != null && attr.getValue().equalsIgnoreCase(e.attribute));
    }

    private boolean matchTagEqualSimple(Node node) {
        String nodeName = (String) this.param;
        return (nodeName.equalsIgnoreCase(node.getName()));
    }

    private boolean matchTagEqualRegex(Node node) {
        String nodeName = (String) this.param;
        return Pattern.compile(nodeName, Pattern.CASE_INSENSITIVE).matcher(node.getName()).matches();
    }

    /**
     * Matches condition against a node.
     */
    public boolean match(Node node) {
        switch (this.type) {
            case C_ATTR_EQUAL_REGEX:
                return matchAttrEqualRegex(node);
            case C_ATTR_EQUAL_SIMPLE:
                return matchAttrEqualSimple(node);
            case C_TAG_NAME_EQUALS_SIMPLE:
                return matchTagEqualSimple(node);
            case C_TAG_NAME_EQUALS_REGEX:
                return matchTagEqualRegex(node);
        }
        return false;
    }

    private static List<BlockElement> parseBlockElements(final String block) {
        List<BlockElement> elements = new LinkedList<BlockElement>();
        for (int p = 0;;) {
            StringUtil u;
            String name = null, value = null;
            u = StringUtil.readText(block, ",=}", StringUtil.READTEXT_OR_CHARS, p);
            if (u.reachEOF()) {
                break;
            }
            char del = u.getDelimiter();
            name = u.getText();
            if (del == '=') {
                u = StringUtil.readText(block, ",}", StringUtil.READTEXT_OR_CHARS, u.getPosition());
                if (u.reachEOF()) {
                    break;
                }
                value = u.getText();
                del = u.getDelimiter();
            }
            elements.add(new BlockElement(name, value));
            if (del == '}') {
                return elements;
            }
            p = u.getPosition();
        }
        return null;
    }

    /**
     * Build a list of conditions from a piece of a path.
     */
    public static List<Condition> makeConditions(final String pathInfo) {
        List<Condition> conditions = new LinkedList<Condition>();
        StringUtil uTag = StringUtil.readText(pathInfo, "{", 0);
        String tagName = uTag.getText();

        if (!tagName.isEmpty()) {
            conditions.add(new Condition(COND_NAME_EQUALS, tagName));
        }
        List<BlockElement> elements = parseBlockElements(pathInfo.substring(uTag.getPosition()));
        if (elements != null) {
            for (BlockElement e : elements) {
                conditions.add(new Condition(COND_ATTRIBUTE_EQUALS, e));
            }
        }

        return conditions;
    }
}

public class Selector {

    public static boolean nodeMatchesPath(Node node, String pathInfo) {
        List<Condition> conditions = Condition.makeConditions(pathInfo);
        for (Condition c : conditions) {
            if (!c.match(node)) {
                return false;
            }
        }
        return true;
    }

    /**
     * Breaks a path into pieces. Pieces are separated by "/" or ">".
     */
    public static Path breakPath(final String path) {
        Path result = new Path();
        StringBuilder piece = new StringBuilder();

        int quote = 0;
        for (int p = 0; p < path.length(); ++p) {
            char c = path.charAt(p);
            int qt = 0;
            int rc = -1;
            if (c == '\'' || c == '"') {
                qt = c;
            }
            if (c == ':' && p + 1 < path.length()) {
                rc = path.charAt(p + 1);
                if (">/:'\"rn".indexOf(rc) != -1) {
                    switch (rc) {
                        case 'r':
                            rc = '\r';
                            break;
                        case 'n':
                            rc = '\n';
                            break;
                    }
                    if (c == '"' || c == '\'') {
                        piece.append(':');
                    }
                    ++p;
                }
            }
            if ((quote == 0 || rc == -1) && (c == '/' || c == '>')) {
                result.getPieces().add(piece.toString());
                result.getSeparators().add(c + "");
                piece = new StringBuilder();
                continue;
            }

            if (rc == -1) {
                rc = c;
            }
            piece.append((char) rc);

            if (qt != 0) {
                if (quote == qt) {
                    quote = 0;
                } else {
                    quote = qt;
                }
            }
        }

        String lastPiece = piece.toString();
        if (!lastPiece.isEmpty()) {
            result.getPieces().add(lastPiece);
        }

        return result;
    }
}

class BlockElement {

    public String name;
    public String attribute;

    public BlockElement(String name, String attribute) {
        this.name = name;
        this.attribute = attribute;
    }
}
